/*
 *  UVCCamera
 *  library and sample to access to UVC web camera on non-rooted Android device
 *
 * Copyright (c) 2014-2017 saki t_saki@serenegiant.com
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *   You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *   Unless required by applicable law or agreed to in writing, software
 *   distributed under the License is distributed on an "AS IS" BASIS,
 *   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *   See the License for the specific language governing permissions and
 *   limitations under the License.
 *
 *  All files in the folder are under this Apache License, Version 2.0.
 *  Files in the libjpeg-turbo, libusb, libuvc, rapidjson folder
 *  may have a different license, see the respective files.
 */

package com.serenegiant.encoder;

import android.media.MediaCodec;
import android.media.MediaFormat;
import android.media.MediaMuxer;
import android.os.Environment;
import android.os.StatFs;
import android.text.TextUtils;
import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.Locale;

public class MediaMuxerWrapper {
	private static boolean DEBUG = true;	// TODO set false on release
	private static final String TAG = "MediaMuxerWrapper";

	private static final String DIR_NAME = "USBCamera";
    private static final SimpleDateFormat mDateTimeFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US);

	private String mOutputPath;
	private final MediaMuxer mMediaMuxer;	// API >= 18
	private int mEncoderCount, mStatredCount;
	private boolean mIsStarted;
	private MediaEncoder mVideoEncoder, mAudioEncoder;

	/**
	 * Constructor
	 * @param ext extension of output file
	 * @throws IOException
	 */
	public MediaMuxerWrapper(int recordMode, String path, String ext) throws IOException {
		if (TextUtils.isEmpty(ext)) ext = ".mp4";
		try {
			mOutputPath = getCaptureFile(recordMode, path, ext).toString();
		} catch (final NullPointerException e) {
			throw new RuntimeException("This app has no permission of writing external storage");
		}
		mMediaMuxer = new MediaMuxer(mOutputPath, MediaMuxer.OutputFormat.MUXER_OUTPUT_MPEG_4);
		mEncoderCount = mStatredCount = 0;
		mIsStarted = false;
	}

	public MediaMuxerWrapper setDebugMode(boolean debug) {
		this.DEBUG = debug;
		return this;
	}

	public String getOutputPath() {
		return mOutputPath;
	}

	public void prepare() throws IOException {
		if (mVideoEncoder != null)
			mVideoEncoder.prepare();
		if (mAudioEncoder != null)
			mAudioEncoder.prepare();
	}

	public void startRecording() {
		if (mVideoEncoder != null)
			mVideoEncoder.startRecording();
		if (mAudioEncoder != null)
			mAudioEncoder.startRecording();
	}

	public void stopRecording() {
		if (mVideoEncoder != null)
			mVideoEncoder.stopRecording();
		mVideoEncoder = null;
		if (mAudioEncoder != null)
			mAudioEncoder.stopRecording();
		mAudioEncoder = null;
	}

	public synchronized boolean isStarted() {
		return mIsStarted;
	}

//**********************************************************************
//**********************************************************************
	/**
	 * assign encoder to this calss. this is called from encoder.
	 * @param encoder instance of MediaVideoEncoder or MediaAudioEncoder
	 */
	/*package*/ void addEncoder(final MediaEncoder encoder) {
		if (encoder instanceof MediaVideoEncoder) {
			if (mVideoEncoder != null)
				throw new IllegalArgumentException("Video encoder already added.");
			mVideoEncoder = encoder;
		} else if (encoder instanceof MediaAudioEncoder) {
			if (mAudioEncoder != null)
				throw new IllegalArgumentException("Video encoder already added.");
			mAudioEncoder = encoder;
		} else
			throw new IllegalArgumentException("unsupported encoder");
		mEncoderCount = (mVideoEncoder != null ? 1 : 0) + (mAudioEncoder != null ? 1 : 0);
	}

	/**
	 * request start recording from encoder
	 * @return true when muxer is ready to write
	 */
	/*package*/ synchronized boolean start() {
		if (DEBUG)
			Log.v(TAG, "start: mStatredCount " + mStatredCount + ", mEncoderCount " + mEncoderCount);
		mStatredCount++;
		if ((mEncoderCount > 0) && (mStatredCount == mEncoderCount)) {
			mMediaMuxer.start();
			mIsStarted = true;
			notifyAll();
			if (DEBUG) Log.v(TAG,  "MediaMuxer started:");
		}
		return mIsStarted;
	}

	/**
	 * 有时候开始录音就走到停止
	 * 两次录音之间需要有间隔;by ljb 20250303;
	 * (Stopping writer thread-Writer thread stopped-The mp4 file will not be streamable.-MediaMuxer stopped-Encoder thread exiting)
	 * request stop recording from encoder when encoder received EOS
	*/
	/*package*/ synchronized void stop() {
		if (DEBUG)
			Log.v(TAG, "stop:mStatredCount=" + mStatredCount + ", mEncoderCount " + mEncoderCount);
		mStatredCount--;
		if ((mEncoderCount > 0) && (mStatredCount <= 0)) {
			try {
				mMediaMuxer.stop();
			} catch (final Exception e) {
				Log.w(TAG, e);
			}
			mIsStarted = false;
			if (DEBUG) Log.v(TAG,  "MediaMuxer stopped:");
		}
	}

	/**
	 * assign encoder to muxer
	 * @param format
	 * @return minus value indicate error
	 */
	/*package*/ synchronized int addTrack(final MediaFormat format) {
		if (mIsStarted)
			throw new IllegalStateException("muxer already started");
		final int trackIx = mMediaMuxer.addTrack(format);
		if (DEBUG) Log.i(TAG, "addTrack:trackNum=" + mEncoderCount + ",trackIx=" + trackIx + ",format=" + format);
		return trackIx;
	}

	/**
	 * write encoded data to muxer
	 * @param trackIndex
	 * @param byteBuf
	 * @param bufferInfo
	 */
	/*package*/ synchronized void writeSampleData(final int trackIndex, final ByteBuffer byteBuf, final MediaCodec.BufferInfo bufferInfo) {
		if (mStatredCount > 0)
			mMediaMuxer.writeSampleData(trackIndex, byteBuf, bufferInfo);
	}

//**********************************************************************
//**********************************************************************

	/**
	 * 视频和图片的保存目录
	 */
	public static final File getCaptureFile(final String type) {
		return new File(Environment.getExternalStoragePublicDirectory(type), DIR_NAME);
	}

    /**
     * generate output file
     * @param type Environment.DIRECTORY_MOVIES / Environment.DIRECTORY_DCIM etc.
     * @param ext .mp4(.m4a for audio) or .png
     * @return return null when this app has no writing permission to external storage.
     */
    public static final File getCaptureFile(final int recordMode, final String path, final String ext) {
		String name = getDateTimeString();// yyyy-MM-dd-HH-mm-ss
		final File dir = recordMode == 0 ? new File(path, name.substring(0, 10)) : new File(path);
		if (DEBUG) Log.d(TAG, "path=" + dir.toString());
		dir.mkdirs();
		if (dir.canWrite()) {
			return new File(dir, getDateTimeString() + ext);
		}
    	return null;
    }

	/**
	 * 优化视频的存储路径
	 * 支持存储空间不足覆盖存储;by ljb 20250303;
	 */
	public static final boolean initMoviesDirectory(int recordMode, String folderPath) {
		final File dir = new File(folderPath);// /storage/emulated/0/Movies/USBCamera
		if (DEBUG)
			Log.d(TAG, "initMoviesDirectory: path=" + dir + ",getFreeSpace=" + (dir != null ? dir.getFreeSpace() : "null"));
		if (dir != null && dir.exists()) {
			if (recordMode != 0) {
				getAvailableSize(dir);
				optimSpace(recordMode, dir);
				return true;
			}
			boolean isLittleSpace = true;
			do {
				long size1 = getAvailableSize(dir);
				if (size1 < 1000) {// 空间不够,至少要 578M
					isLittleSpace = optimSpace(recordMode, dir);
				} else {
					isLittleSpace = false;
				}
			} while (isLittleSpace);
			return true;
		}
		return false;
	}

	/**
	 * get current date and time as String
	 * @return
	 */
	public static final String getDateTimeString() {
    	final GregorianCalendar now = new GregorianCalendar();
    	return mDateTimeFormat.format(now.getTime());
    }

	/**
	 * 获取文件剩下空间
	 */
	public static long getAvailableSize(File dir) {
		StatFs statFs = new StatFs(dir.getPath());
		long totalBlocks = statFs.getBlockCountLong();
		long availableBlocks = statFs.getAvailableBlocksLong();
		long blockSize = statFs.getBlockSizeLong();
		long size1 = availableBlocks * blockSize / (1024 * 1024);// 获取文件系统可用大小
		long size2 = totalBlocks * blockSize / (1024 * 1024) / 1024;// 获取文件系统总大小
		if (DEBUG)
			Log.d(TAG, "initMoviesDirectory: 剩余空间 =" + size1 + "M,全部空间 =" + size2 + "G");
		return size1;
	}

	/**
	 * 优化空间
	 */
	public static boolean optimSpace(int recordMode, File dir) {
		File[] list1 = dir.listFiles();
		if (list1 == null && list1.length < 2) {
			return false;
		}
		int length = list1.length;
		List<File> list2 = Arrays.asList(list1);
		Collections.sort(list2, new Comparator<File>() {
			@Override
			public int compare(File o1, File o2) {
				return Long.compare(o1.lastModified(), o2.lastModified());
			}
		});
		if (recordMode == 0) {
			Log.d(TAG, "delete file name:" + list2.get(0).getName());
			deleteDirs(list2.get(0));
		} else {
			for (int i = 0; i < length - 1; i++) {
				Log.d(TAG, "delete file 第" + i + "/" + length + "个:" + list2.get(i).getName());
				deleteDirs(list2.get(i));
			}
		}
		return true;
	}

	/**
	 * 递归删除文件夹及所有内容
	 */
	public static void deleteDirs(File file) {
		if (file.isDirectory()) {
			File[] files = file.listFiles();
			if (files != null) {
				for (File child : files) {
					deleteDirs(child); // 递归删除子项
				}
			}
		}
		file.delete(); // 删除文件或空文件夹
	}
}
